
//
// 101019 - AH - Created.
// 101215 - AH - Moved from org.soekrat
// 101228 - AH - Minor java doc fix, bug discovered by java doc compiler.
// 120307 - AH - Moved from org.soekrat.gui.swing.util : Command line gui should
//               be available in all context.
//

package org.soekrat.engine.util;

import java.io.CharArrayWriter;
import java.io.File;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Hashtable;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * <p>
 *   Simple command line parser for
 *   {@code -<option> [value]} formated command lines.
 * </p>
 * <p>
 *   Does not do any validation.
 *
 *   Values without any option will be ignored.
 * </p>
 * @author Arne Halvorsen (AH)
 * @author Laetitia Le Chatton (LLC)
 */
public final class CommandLineParser
{
  private Hashtable<String, Object> m_args=new Hashtable<String, Object>();

  // Hashtable (unlike java 2 Maps) does not allow null values, uses this.
  private static final Object NO_ARG=new Object();
  
  private final int m_errorCode;
  
  /**
   * <p>
   *   Constructor.
   * </p>
   * <p>
   *   The
   *   {@link #getErrorCode() error code} property is {@code -1}.
   * </p>
   * @param args Command line argument string.
   */
  public CommandLineParser(String args){ this(args, -1); }

  /**
   * <p>
   *   Constructor.
   * </p>
   * @param args      Command line argument string.
   * @param errorCode {@link #getErrorCode()}.
   */
  public CommandLineParser(String args, int errorCode)
  {
    if (args==null)
    {
      throw new NullPointerException("args");
    }
    if (errorCode==0)
    {
      throw new IllegalArgumentException("errorCode==0");
    }
    
    m_errorCode=errorCode;
    
    StringTokenizer st=new StringTokenizer(args);
    String[] a=new String[st.countTokens()];
    int i=0;
    while (st.hasMoreTokens()) a[i++]=st.nextToken();
    initialize(a);
  }
  
  /**
   * <p>
   *   Constructor.
   * </p>
   * <p>
   *   The
   *   {@link #getErrorCode() error code} property is {@code -1}.
   * </p>
   * @param args Command line arguments to parse.
   */
  public CommandLineParser(String[] args){ this(args, -1); }
  
  /**
   * <p>
   *   Constructor.
   * </p>
   * @param args      Command line arguments to parse.
   * @param errorCode {@link #getErrorCode()}.
   */
  public CommandLineParser(String[] args, int errorCode)
  {
    if (args==null)
    {
      throw new NullPointerException("args");
    }

    m_errorCode=errorCode;
    
    initialize(args);
  }
  
  /**
   * <p>
   *   Gets error code passed to
   *   {@link System#exit(int)} when a mandatory option is missing.
   * </p>
   * @return Error code.
   */
  public int getErrorCode(){ return m_errorCode; }
  
  // Invoked from constructors.
  private void initialize(String[] args)
  {
    String current=null;
    for (int i=0; i<args.length; i++)
    {
      if (args[i].charAt(0)=='-')
      {
        if (current!=null) m_args.put(current, NO_ARG);
        current=args[i];
      }
      else if (current!=null)
      {
        m_args.put(current, args[i]);
        current=null;
      }
    }
    if (current!=null) m_args.put(current, NO_ARG);
  }

  /**
   * <p>
   *   Tells if given option exists.
   * </p>
   * @param option Name on option.
   * @return True if option exists.
   */
  public boolean exists(String option)
  {
    if (option==null)
    {
      throw new NullPointerException("option");
    }

    String o=(option.charAt(0)=='-') ? option : '-'+option;
    return m_args.containsKey(o);
  }
  
  /**
   * <p>
   *   If mandatory option not given 
   *   {@link System#exit(int) exits} the application with the 
   *   {@link #getErrorCode() given error} code if not found.
   * </p>
   * @param option Option.
   */
  public void mandatoryExists(String option)
  {
    if (!exists(option))
    {
      option=option.startsWith("-") ? option : "-"+option;
      System.err.println("Missing option: "+option);
      System.exit(m_errorCode);
    }
  }

  /**
   * <p>
   *   Gets option value.
   * </p>
   * @param option Option.
   * @return Value or {@code null} null if option have no value or do not
   *         exists.
   */
  public String value(String option)
  {
    if (option==null)
    {
      throw new NullPointerException("option");
    }

    String o=(option.charAt(0)=='-') ? option : '-'+option;
    Object v=m_args.get(o);
    return (v==NO_ARG || v==null) ? null : v.toString();
  }
  
  /**
   * <p>
   *   Invoked for options that has an optional value argument. 
   * </p>
   * @param option Option.
   * @return Value if option given with argument value, the empty 
   *         {@code String} if option given with no argument value and
   *         {@code null} if option not given. 
   */
  public String valueWithOptArg(String option)
  {
    if (option==null)
    {
      throw new NullPointerException("option");
    }

    String o=(option.charAt(0)=='-') ? option : '-'+option;
    Object v=m_args.get(o);
    if (v==null) return null;
    return v==NO_ARG ? "" : v.toString(); 
  }

  /**
   * <p>
   *   Gets option value.
   * </p>
   * @param option Option.
   * @param def    Default value to return if option value not given.
   * @return Value or {@code null} if option have no value or does not exists.
   */
  public String value(String option, String def)
  {
    if (option==null)
    {
      throw new NullPointerException("option");
    }

    String o=(option.charAt(0)=='-') ? option : '-'+option;
    Object v=m_args.get(o);
    return (v==NO_ARG || v==null) ? def : v.toString();
  }
  
  /**
   * <p>
   *   Gets mandatory option value or 
   *   {@link System#exit(int) exits} the application with the
   *   {@link #getErrorCode() given error} code if not found.
   * </p>
   * @param option Option.
   * @return Option value.
   */
  public String mandatoryValue(String option)
  { 
    String retVal=value(option);
   
    if (retVal==null)
    {
      option=getOptionName(option);
      System.err.println("Missing option "+option+" or option's value");
      System.exit(m_errorCode);
    }
    
    return retVal;    
  }
  
  /**
   * <p>
   *   Gets boolean option value 
   *   ({@link System#exit(int) exits} the application with the 
   *   {@link #getErrorCode() given error code} if value is not an boolean).
   * </p>
   * <p>
   *   Values that maps to {@code true} are "true" and "yes" case ignored and
   *   values that maps to {@code false} are "false" and "no" case ignored.
   * </p>
   * @param option Option.
   * @param def    Default value.
   * @return Option value.
   */
  public boolean booleanValue(String option, boolean def)
  {
    String val=value(option);
    return val==null ? def : returnBoolean(option, val);
  }
  
  /**
   * <p>
   *   Gets mandatory boolean option value 
   *   ({@link System#exit(int) exits} if value is not an boolean) or does not
   *   exists 
   *   {@link System#exit(int) exits} the application with the 
   *   {@link #getErrorCode() given error} code.
   * </p>
   * <p>
   *   Values that maps to {@code true} are "true" and "yes" case ignored and
   *   values that maps to {@code false} are "false" and "no" case ignored.
   * </p> 
   * @param option Option.
   * @return Option value.
   */
  public boolean mandatoryBooleanValue(String option)
  {
    return returnBoolean(option, mandatoryValue(option));
  }
  
  // Invoked from methods returning boolean option values.
  private boolean returnBoolean(String option, String val)
  {
    if (val.equalsIgnoreCase("true") || val.equalsIgnoreCase("yes")) 
      return true;
    if (val.equalsIgnoreCase("false") || val.equalsIgnoreCase("no")) 
      return false;
    option=getOptionName(option);
    System.err.println("Option "+option+" value "+val+
      " must be true, yes, false or no (case ignored)");
    System.exit(m_errorCode);
    return false; // Only to compile, can never come here.
  }
  
  /**
   * <p>
   *   Gets integer option value 
   *   ({@link System#exit(int) exits} if value is not an integer).
   * </p>
   * @param option Option.
   * @param def    Default value.
   * @return Option value.
   */
  public int intValue(String option, int def)
  {
    String val=value(option);
    return val==null ? def : returnInt(option, val);
  }
  
  /**
   * <p>
   *   Gets integer option value.
   * </p>
   * @param option Option.
   * @return Value or {@code null} if not found.
   */
  public Integer intValue(String option)
  {
    String val=value(option);
    return val==null ? null : new Integer(val);
  }
  
  /**
   * <p>
   *   Gets mandatory integer option value 
   *   ({@link System#exit(int) exits} if value is not an integer) or 
   *   {@link System#exit(int) exits} the application with the 
   *   {@link #getErrorCode() given error} code.
   * </p>
   * @param option Option.
   * @return Option value.
   */
  public int mandatoryIntValue(String option)
  { 
    return returnInt(option, mandatoryValue(option)); 
  }
  
  /**
   * <p>
   *   Gets double option value 
   *   ({@link System#exit(int) exits} if value is not a double).
   * </p>
   * @param option Option.
   * @param def    Default value.
   * @return Option value.
   */
  public double doubleValue(String option, double def)
  {
    String val=value(option);
    return val==null ? def : returnDouble(option, val);
  }
  
  /**
   * <p>
   *   Gets double option value.
   * </p>
   * @param option Option.
   * @return Value or {@code null} if not found.
   */
  public Double doubleValue(String option)
  {
    String val=value(option);
    return val==null ? null : new Double(val);
  }
  
  /**
   * <p>
   *   Gets mandatory double option value 
   *   ({@link System#exit(int) exits} if value is not a double) or 
   *   {@link System#exit(int) exits} the application with the 
   *   {@link #getErrorCode() given error} code.
   * </p>
   * @param option Option.
   * @return Option value.
   */
  public double mandatoryDoubleValue(String option)
  { 
    return returnDouble(option, mandatoryValue(option)); 
  }
  
  /**
   * <p>
   *   Gets file value.
   * </p>
   * @param option Option.
   * @return Option value or {@code null} if not found.
   */
  public File fileValue(String option)
  {
    String val=value(option);
    return val==null ? null : new File(val);
  }
  
  /**
   * <p>
   *   Gets file value.
   * </p>
   * @param option Option.
   * @param def    Default value.
   * @return Option value.
   */
  public File fileValue(String option, String def)
  {
    String val=value(option);
    
    if (def==null)
    {
      throw new NullPointerException("def");  
    }
        
    return val==null ? new File(def) : new File(val);
  }
  
  /**
   * <p>
   *   Gets file value.
   * </p>
   * @param option Option.
   * @param def    Default value.
   * @return Option value.
   */
  public File fileValue(String option, File def)
  {
    String val=value(option);
    
    if (def==null)
    {
      throw new NullPointerException("def");  
    }
        
    return val==null ? def : new File(val);
  }
  
  /**
   * <p>
   *   Gets mandatory file value.
   * </p>
   * @param option Option.   
   * @return Option value.
   */
  public File mandatoryFileValue(String option)
  { 
    return new File(mandatoryValue(option));
  }
  
  /**
   * <p>
   *   Gets file value.
   * </p>
   * @param option Option.
   * @return Option value or {@code null} if not found.
   */
  public URL urlValue(String option)
  {
    String val=value(option);
    return val==null ? null : returnUrl(option, val);
  }
  
  /**
   * <p>
   *   Gets file value.
   * </p>
   * @param option Option.
   * @param def    Default value.
   * @return Option value.
   */
  public URL urlValue(String option, String def)
  {
    String val=value(option);
    
    if (def==null)
    {
      throw new NullPointerException("def");  
    }
        
    String spec=(val==null) ? def : val;
    return returnUrl(option, mandatoryValue(spec));
  }
  
  /**
   * <p>
   *   Gets file value.
   * </p>
   * @param option Option.
   * @param def    Default value.
   * @return Option value.
   */
  public URL urlValue(String option, URL def)
  {
    String val=value(option);
    
    if (def==null)
    {
      throw new NullPointerException("def");  
    }
        
    return val==null ? def : returnUrl(option, mandatoryValue(option));
  }
  
  /**
   * <p>
   *   Gets mandatory file value.
   * </p>
   * @param option Option.   
   * @return Option value.
   */
  public URL mandatoryUrlValue(String option)
  { 
    return returnUrl(option, mandatoryValue(option));
  }
  
  // Invoked from methods returning URL option values.
  private URL returnUrl(String option, String val)
  {
    try 
    {
      return new URL(val);
    }
    catch (MalformedURLException mux)
    {
      option=getOptionName(option);
      System.err.println("Option "+option+" value "+val+" not an URL");
      System.exit(m_errorCode);
      return null; // Only to compile, can never come here.
    }
  }
  
  // Invoked from methods returning int option values.
  private int returnInt(String option, String val)
  {
    try 
    {
      return Integer.parseInt(val);
    }
    catch (NumberFormatException nfx)
    {
      option=getOptionName(option);
      System.err.println("Option "+option+" value "+val+" not an integer");
      System.exit(m_errorCode);
      return 0; // Only to compile, can never come here.
    }
  }
  
  // Invoked from methods returning double option values.
  private double returnDouble(String option, String val)
  {
    try 
    {
      return Double.parseDouble(val);
    }
    catch (NumberFormatException nfx)
    {
      option=getOptionName(option);
      System.err.println("Option "+option+" value "+val+" not a double");
      System.exit(m_errorCode);
      return 0; // Only to compile, can never come here.
    }
  }
  
  // Ensures writes -option in error messages.
  private static String getOptionName(String option)
  {
    return option.startsWith("-") ? option : "-"+option;
  }

  /**
   * <p>
   *   For debug purpose only.
   * </p>
   * @return {@code String} presentation.
   */
  @Override
  public String toString()
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    for (Map.Entry<String, Object> e : m_args.entrySet())
    {      
      pw.print(e.getKey());
      Object value=e.getValue();
      if (value!=null) pw.print("="+value);
      pw.println();
    }
        
    pw.flush();
    return caw.toString();
  }

}
